/*------------------------------------------------------------------------------
 * Copyright 2012 NCR Corporation
 *------------------------------------------------------------------------------
 * All revision information is updated automatically from source code control
 * change records - please do not manually edit.
 *------------------------------------------------------------------------------
 *
 *  $LastChangedRevision::                                          $
 *  $LastChangedBy::                                                $
 *  $LastChangedDate::                                              $
 */

package com.ncr.nep.dashboardcore.ui.client;

import java.util.HashMap;

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Manager used to provide browser-independent drag and drop support to
 * {@link Widget} instances.
 *
 * @author rb250096
 * @version $Revision$
 */
public class DragManager implements MouseUpHandler, MouseMoveHandler {
    
    //--------------------------------------------------------------------------
    //
    //  Fields
    //
    //--------------------------------------------------------------------------

    /**
     * The singleton instance of the manager.
     */
    private static DragManager instance = new DragManager();

    /**
     * Z-index of object being dragged.
     */
    protected int zIndex = 100000;

    /**
     * The {@link Widget} that is being dragged.
     */
    protected IsWidget initiator;

    /**
     * Style object for the {@link #initiator}.
     */
    protected Style initiatorStyle;

    /**
     * Start X coordinate in pixels of the drag operation.
     */
    protected int dragStartX;

    /**
     * Start Y coordinate in pixels of the drag operation.
     */
    protected int dragStartY;

    /**
     * The left coordinate of the body.
     */
    protected int clientLeft;

    /**
     * The top coordinate of the body.
     */
    protected int clientTop;

    /**
     * The width of the browser window in pixels.
     */
    protected int windowWidth;

    /**
     * Registration object for the mouse up event on the initiator.
     */
    protected HandlerRegistration mouseUpRegistration;

    /**
     * Registration object for the mouse move event on the initiator.
     */
    protected HandlerRegistration mouseMoveRegistration;

    /**
     * Map of potential drop targets to their corresponding drag over handler.
     */
    protected HashMap<IsWidget, DragOverHandler> targets =
            new HashMap<IsWidget, DragOverHandler>();

    /**
     * The {@link Widget} drop target that the mouse is currently over.
     */
    protected IsWidget currentDropWidget;

    /**
     * The {@link Widget} drop target that has accepted the drop after being
     * hovered over.
     */
    protected IsWidget acceptedDropWidget;

    /**
     * Completion handler called when the drag completes and a drop target
     * accepts the drag.
     */
    protected DragCompleteHandler completeHandler;

    /**
     * Cached width of initiator used to reset dragged item when drag is
     * complete.
     */
    protected String cachedWidth;

    /**
     * Cached height of initiator used to reset dragged item when drag is
     * complete.
     */
    protected String cachedHeight;

    /**
     * Cached position of initiator used to reset dragged item when drag is
     * complete.
     */
    protected String cachedPosition;

    //--------------------------------------------------------------------------
    //
    //  Constructors
    //
    //--------------------------------------------------------------------------

    /**
     * Constructor
     */
    protected DragManager() { }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  Publicly available methods
    //----------------------------------

    /**
     * Starts a drag operation.
     *
     * @param initiator The object being dragged.
     * @param completeHandler Handle invoked when the drag is complete.
     * @param mouseEvent The mouse event that initiated the drag.
     * @param absoluteLeft The absolute left position of the initiator in
     *                     pixels.
     * @param absoluteTop The absolute top position of the initiator in pixels.
     */
    public static void doDrag(IsWidget initiator,
                              DragCompleteHandler completeHandler,
                              MouseEvent<?> mouseEvent,
                              int absoluteLeft,
                              int absoluteTop) {
        instance.startDrag(initiator, completeHandler, mouseEvent,
                absoluteLeft, absoluteTop);
    }

    /**
     * Registers a drop target that can accept a drop.
     *
     * @param target The {@link Widget} that can accept a drop.
     * @param overHandler The handle invoked when the object being dragged is
     *                    dragged over target.
     */
    public static void registerDropTarget(IsWidget target,
                                          DragOverHandler overHandler) {
        instance.addDragTarget(target, overHandler);
    }

    /**
     * Unregisters a drop target.
     *
     * @param target The {@link Widget} to unregister.
     */
    public static void unregisterDropTarget(IsWidget target) {
        instance.removeDragTarget(target);
    }

    /**
     * Tells the manager that the provided {@link Widget} can accept the drag
     * operation.
     *
     * @param target The {@link Widget} that is accepting the drop.
     */
    public static void acceptDrop(IsWidget target) {
        instance.accept(target);
    }

    //----------------------------------
    //  Internal instance methods
    //----------------------------------

    /**
     * Starts a drag operation.
     *
     * @param initiator The object being dragged.
     * @param completeHandler Handle invoked when the drag is complete.
     * @param mouseEvent The mouse event that started the drag.
     * @param absoluteLeft The absolute left position of the initiator in
     *                     pixels.
     * @param absoluteTop The absolute top position of the initiator in pixels.
     */
    protected void startDrag(IsWidget initiator,
                             DragCompleteHandler completeHandler,
                             MouseEvent<?> mouseEvent,
                             int absoluteLeft,
                             int absoluteTop) {
        this.initiator = initiator;
        this.completeHandler = completeHandler;
        
        this.clientLeft = Document.get().getBodyOffsetLeft();
        this.clientTop = Document.get().getBodyOffsetTop();
        this.windowWidth = Window.getClientWidth();

        Widget initiatorWidget = this.initiator.asWidget();
        this.mouseUpRegistration = initiatorWidget.addDomHandler(this,
                MouseUpEvent.getType());
        this.mouseMoveRegistration = initiatorWidget.addDomHandler(
                this, MouseMoveEvent.getType());

        if(DOM.getCaptureElement() == null) {
            DOM.setCapture(initiatorWidget.getElement());
        }

        Element initiatorElement = initiatorWidget.getElement();
        int width = initiatorElement.getClientWidth();
        int height = initiatorElement.getClientHeight();

        this.dragStartX = mouseEvent.getClientX() + Window.getScrollLeft();
        this.dragStartY = mouseEvent.getClientY() + Window.getScrollTop();

        this.initiatorStyle = initiatorElement.getStyle();

        //Cache existing styles that need to be reset after drag.
        this.cachedWidth = this.initiatorStyle.getWidth();
        this.cachedHeight = this.initiatorStyle.getHeight();
        this.cachedPosition = this.initiatorStyle.getPosition();

        this.initiatorStyle.setWidth(width, Style.Unit.PX);
        this.initiatorStyle.setHeight(height, Style.Unit.PX);
        this.initiatorStyle.setZIndex(this.zIndex);
        this.initiatorStyle.setPosition(Style.Position.ABSOLUTE);
        this.initiatorStyle.setCursor(Style.Cursor.MOVE);

        this.move(absoluteLeft, absoluteTop);
        RootPanel.get().add(this.initiator);
    }

    /**
     * Ends the drag operation.
     */
    protected void endDrag() {
        this.mouseMoveRegistration.removeHandler();
        this.mouseUpRegistration.removeHandler();
        
        DOM.releaseCapture(this.initiator.asWidget().getElement());

        //Reset styles
        this.initiatorStyle.clearZIndex();
        this.initiatorStyle.clearCursor();

        if(this.cachedWidth != null) {
            this.initiatorStyle.setProperty("width", this.cachedWidth);
        }
        else {
            this.initiatorStyle.clearWidth();
        }

        if(this.cachedHeight != null) {
            this.initiatorStyle.setProperty("height", this.cachedHeight);
        }
        else {
            this.initiatorStyle.clearHeight();
        }

        if(this.cachedPosition != null) {
            this.initiatorStyle.setProperty("position", this.cachedPosition);
        }
        else {
            this.initiatorStyle.clearPosition();
        }

        this.initiator.asWidget().removeFromParent();

        if(this.acceptedDropWidget != null && this.currentDropWidget != null &&
                !this.currentDropWidget.equals(this.acceptedDropWidget)) {
            this.acceptedDropWidget = null;
        }
        this.completeHandler.onDragComplete(
                new DragCompleteEvent(this.initiator, this.acceptedDropWidget));

        this.currentDropWidget = null;
        this.acceptedDropWidget = null;
        this.initiator = null;
        this.initiatorStyle = null;
    }

    /**
     * Adds a drop target that can accept a drop.
     *
     * @param target The {@link Widget} that can accept a drop.
     * @param overHandler The handle invoked when the object being dragged is
     *                    dragged over target.
     */
    protected void addDragTarget(IsWidget target, DragOverHandler overHandler) {
        if(!this.targets.containsKey(target)) {
            this.targets.put(target, overHandler);
        }
    }

    /**
     * Un-registers a drop target.
     *
     * @param target The {@link Widget} to un-register.
     */
    protected void removeDragTarget(IsWidget target) {
        if(this.targets.containsKey(target)) {
            this.targets.remove(target);
        }
    }

    /**
     * Tells the manager that the provided {@link Widget} can accept the drag
     * operation.
     *
     * @param target The {@link Widget} that is accepting the drop.
     */
    protected void accept(IsWidget target) {
        this.acceptedDropWidget = target;
    }

    /**
     * Moves the object being dragged to the specified left and top coordinates.
     *
     * @param left The left coordinate in pixels.
     * @param top The top coordinate in pixels.
     */
    protected void move(int left, int top) {
        // Account for the difference between absolute position and the
        // body's positioning context.
        left -= Document.get().getBodyOffsetLeft();
        top -= Document.get().getBodyOffsetTop();

        this.initiatorStyle.setLeft(left, Style.Unit.PX);
        this.initiatorStyle.setTop(top, Style.Unit.PX);
    }

    /**
     * Updates manager and dispatches {@link DragOverEvent} for a {@link Widget}
     * that has been hovered over during a drag.
     *
     * @param target The {@link Widget} that is being hovered over.
     * @param event The mouse event that generated the mouse over drop target
     *              occurrence.
     */
    protected void mouseOverDropTarget(IsWidget target, MouseMoveEvent event) {
        this.currentDropWidget = target;
        this.targets.get(target).onDragOver(new DragOverEvent(
                this.initiator, target, event));
    }

    /**
     * Checks to see if the mouse is currently hovering over one of the
     * registered drop targets.  If a match is found, the manager will
     * update event listeners listening for drag over events.
     *
     * @param e The MouseMoveEvent that was received during the drag.
     * @param x The x position of the mouse in the viewport.
     * @param y The y position of the mouse in the viewport.
     */
    protected void checkForMouseOver(MouseMoveEvent e, int x, int y) {
        for(IsWidget target : this.targets.keySet()) {
            if(target.equals(this.initiator)) {
                continue;
            }
            
            Element targetElement = target.asWidget().getElement();
            int left = targetElement.getAbsoluteLeft();
            int top = targetElement.getAbsoluteTop();
            int right = targetElement.getAbsoluteRight();
            int bottom = targetElement.getAbsoluteBottom();
            
            if(x >= left && x <= right && y >= top && y <= bottom) {
                this.mouseOverDropTarget(target, e);
                return;
            }
        }

        this.currentDropWidget = null;
        this.acceptedDropWidget = null;
    }

    //--------------------------------------------------------------------------
    //
    //  Event handlers
    //
    //--------------------------------------------------------------------------

    @Override
    public void onMouseMove(MouseMoveEvent e) {
        int x = e.getClientX() + Window.getScrollLeft();
        int y = e.getClientY() + Window.getScrollTop();

        int diffX = x - this.dragStartX;
        int diffY = y - this.dragStartY;

        this.dragStartX = x;
        this.dragStartY = y;

        int left = this.initiator.asWidget().getAbsoluteLeft() + diffX;
        int top = this.initiator.asWidget().getAbsoluteTop() + diffY;
        
        //If mouse has moved outside bounds of client browser, just return.
        if (left < clientLeft || left >= windowWidth || top < clientTop) {
            return;
        }

        this.move(left, top);

        this.checkForMouseOver(e, x, y);
    }

    @Override
    public void onMouseUp(MouseUpEvent e) {
        this.endDrag();
    }
}